package com.ruoyi.workflow.service.impl;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.business.domain.ServiceItem;
import com.ruoyi.business.service.IServiceItemService;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.workflow.domain.CarPackageAudit;
import com.ruoyi.workflow.mapper.CarPackageAuditMapper;
import com.ruoyi.workflow.service.IBpmnNodeService;
import com.ruoyi.workflow.service.ICarPackageAuditService;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.image.ProcessDiagramGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 套餐审核Service业务层处理
 *
 * @author shier
 * @date 2022-11-17
 */
@Service
public class CarPackageAuditServiceImpl extends ServiceImpl<CarPackageAuditMapper, CarPackageAudit> implements ICarPackageAuditService {

    @Autowired
    private IServiceItemService serviceItemService;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private IBpmnNodeService bpmnNodeService;
    @Autowired
    private ProcessDiagramGenerator processDiagramGenerator;
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private HistoryService historyService;
    @Autowired
    private ISysUserService userService;

    /**
     * 查询套餐审核列表
     *
     * @param carPackageAudit 套餐审核
     * @return 套餐审核
     */
    @Override
    public List<CarPackageAudit> selectCarPackageAuditList(CarPackageAudit carPackageAudit) {
        return getBaseMapper().selectCarPackageAuditList(carPackageAudit);
    }

    /**
     * 在服务项里发起审核
     *
     * @param serviceItemId 服务项ID
     * @param info          审核备注
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void audit(Long serviceItemId, String info) {
        // 1. 根据服务项 id 查询服务项对象
        ServiceItem serviceItem = serviceItemService.get(serviceItemId);

        // 2. 校验审核合法性(是套餐 && 状态为初始化), 提示当前无法审核
        if (ServiceItem.CARPACKAGE_NO.equals(serviceItem.getCarPackage())) {
            throw new ServiceException("当前服务项不是套餐,无法发起审核!!!");
        }

        if (!ServiceItem.AUDITSTATUS_INIT.equals(serviceItem.getAuditStatus())) {
            throw new ServiceException("当前服务项的状态不是初始化,无法发起审核!!!");
        }

        // 3. 修改服务项状态为审核中
        serviceItemService.changeStatus(serviceItemId, ServiceItem.AUDITSTATUS_AUDITING);

        // 4. 创建一条审核记录信息, 设置相关数据
        CarPackageAudit carPackageAudit = new CarPackageAudit();

        // 4.1 将服务项信息(id, name, price, info)设置到审核记录中
        carPackageAudit.setServiceItemId(serviceItemId);                     // 服务项的id
        carPackageAudit.setServiceItemName(serviceItem.getName());           // 服务项的name
        carPackageAudit.setServiceItemPrice(serviceItem.getDiscountPrice()); // 服务项的价格
        carPackageAudit.setServiceItemInfo(serviceItem.getInfo());           // 服务项的备注

        // 4.2 获取到当前登录用户 id 并设置到审核记录中( 使用 Security 提供的工具类得到当前登录用户的id )
        carPackageAudit.setCreatorId(SecurityUtils.getUserId() + "");

        // 4.3 将审核状态设置为进行中
        carPackageAudit.setStatus(CarPackageAudit.PACKAGE_AUDIT_STATUS_PENDING);
        // 4.4 将创建时间设置进去
        carPackageAudit.setCreateTime(new Date());
        // 4.5 将 审核备注 设置进去
        carPackageAudit.setInfo(info);

        // 4.4 保存审核记录信息
        super.save(carPackageAudit);

        // 5. 构建流程变量(BusinessKey/金额/审核人员)
        Map<String, Object> map = new HashMap<>();

        // 5.1 BusinessKey = 审核记录的 id
        String businessKey = carPackageAudit.getId() + "";

        // 5.2 金额 = 服务项的折扣金额
        map.put("money", serviceItem.getDiscountPrice().longValue() + "");

        // 6. 基于流程定义 Key, 任务ID, 参数Map 开启流程实例, 得到流程实例对象
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(CarPackageAudit.CAR_PACKAGE_DEFINITION_KEY, businessKey, map);

        // 6.1 将流程实例 id 设置到审核记录中
        carPackageAudit.setInstanceId(processInstance.getId());

        // 6.2 更新审核记录表
        super.updateById(carPackageAudit);

        // 7: 为当前任务设置候选人: 基于流程实例查询当前进行中的任务
        Task currentTask = taskService.createTaskQuery()
                .processInstanceId(processInstance.getId())
                .singleResult();

        // 7.1: 基于任务 Key 去查询候选人列表
        List<SysUser> userList = bpmnNodeService.selectUsersByNodeKey(currentTask.getTaskDefinitionKey());
        for (SysUser sysUser : userList) {
            // 7.2: 调用 activiti 的 API 为指定任务设置候选人
            taskService.addCandidateUser(currentTask.getId(), sysUser.getUserName());
        }
    }

    /**
     * 审核功能实现
     *
     * @param id     审核记录ID
     * @param taskId 任务ID
     * @param result 审核结果
     * @param info   批注信息
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void doAudit(Long id, String taskId, boolean result, String info) {
        //1. 根据审核记录id获取审核记录对象，并判断状态为审核中才允许审核
        CarPackageAudit audit = getById(id);
        if (!CarPackageAudit.PACKAGE_AUDIT_STATUS_PENDING.equals(audit.getStatus())) {
            throw new ServiceException("非法操作");
        }

        //2. 基于当前用户名作为候选人/负责人条件查询任务信息，如果查询不到提示非法操作
        String loginUsername = SecurityUtils.getUsername();

        Task task = taskService.createTaskQuery()
                .taskId(taskId)
                .taskCandidateOrAssigned(loginUsername)
                .singleResult();
        if (task == null) {
            throw new ServiceException("非法操作, 没有需要进行审核的任务!!!!");
        }

        //3. 设置流程变量（审核结果)
        // 流程变量名 == 任务Key
        taskService.setVariable(taskId, task.getTaskDefinitionKey(), result); // 设置流程变量

        //4. 领取任务/完成任务/添加批注
        taskService.claim(taskId, loginUsername); // 领取任务
        taskService.addComment(taskId, task.getProcessInstanceId(), info); // 添加审批批注
        taskService.complete(taskId);

        //5. 基于流程实例查询下一个进行中的任务，根据是香有下一个任务判断流程是否结束
        Task nextTask = taskService.createTaskQuery()
                .processInstanceId(task.getProcessInstanceId())
                .singleResult();

        if (nextTask != null) {
            //6. 有下一个任务
            //6.1. 判断当前审核结果是通过还是拒绝
            if (result) {
                //6.2. 如果是审核通过，基于任务ky查询候选人列表
                List<SysUser> userList = bpmnNodeService.selectUsersByNodeKey(nextTask.getTaskDefinitionKey());
                //6.2.1. 为下一个任务设置候选人
                for (SysUser sysUser : userList) {
                    taskService.addCandidateUser(nextTask.getId(), sysUser.getUserName());
                }
            } else {
                //6.3. 如果是审核拒绝，为任务设置选人为发起人
                //6.3.1. 通过审核记录对象的创建1d,查询创建人用户对象
                SysUser sysUser = userService.selectUserById(Long.valueOf(audit.getCreatorId()));

                //6.3.2. 将创建人对象的用户名设置到任务的候选人
                taskService.addCandidateUser(nextTask.getId(), sysUser.getUserName());

                //6.3.3. 更新服务项的状态为重新调整
                serviceItemService.changeStatus(audit.getServiceItemId(), ServiceItem.AUDITSTATUS_REPLY);

                // 6.3.4: 更新审核记录的状态为审核拒绝
                audit.setStatus(CarPackageAudit.PACKAGE_AUDIT_STATUS_REJECTED);
                this.updateById(audit);
            }
        } else {
            //7. 没有任务，在我们的流程中代表结果一定为通过，且流程结束
            //7.1. 修改服务项状态为审核通过
            serviceItemService.changeStatus(audit.getServiceItemId(), ServiceItem.AUDITSTATUS_APPROVED);
            //7.2. 修改审核记录状态为审核通过
            audit.setStatus(CarPackageAudit.PACKAGE_AUDIT_STATUS_PASSED);
            this.updateById(audit);
        }
    }

    @Override
    public void reapply(Long id, String taskId) {
        // 1: 基于 审核记录ID 查询 审核记录对象
        CarPackageAudit audit = this.getById(id);

        // 2: 验证当前状态是否是审核拒绝, 如果不是直接抛出异常
        if (!CarPackageAudit.PACKAGE_AUDIT_STATUS_REJECTED.equals(audit.getStatus())) {
            throw new ServiceException("非法操作, 当前状态不是审核拒绝");
        }

        // 3: 基于 任务ID + 当前用户名 查询任务对象, 确认是否是审核人
        String loginUsername = SecurityUtils.getUsername();
        Task task = taskService.createTaskQuery()
                .taskId(taskId)
                .taskCandidateOrAssigned(loginUsername)
                .singleResult();

        if (task == null) {
            throw new ServiceException("非法操作, 必须要有任务!!!");
        }

        // 5: 领取并完成任务
        taskService.claim(taskId, loginUsername);
        taskService.complete(taskId);

        // 6: 查询下一个代办任务
        Task nextTask = taskService.createTaskQuery()
                .processInstanceId(task.getProcessInstanceId())
                .singleResult();
        // 6.1: 设置流程变量( 使用其他方法设置流程变量不会进行覆盖, 所以只能从任务上直接进行设置 )
        taskService.setVariable(nextTask.getId(), "money", audit.getServiceItemPrice().longValue() + "");


        // 7: 基于 下一个任务 查询 候选人列表
        List<SysUser> userList = bpmnNodeService.selectUsersByNodeKey(nextTask.getTaskDefinitionKey());

        // 8: 为该任务设置候选人
        for (SysUser sysUser : userList) {
            taskService.addCandidateUser(nextTask.getId(), sysUser.getUserName());
        }

        // 9: 重新将 服务项/审核记录 的状态修改为 审核中
        // 9.1: 服务项
        serviceItemService.changeStatus(audit.getServiceItemId(), ServiceItem.AUDITSTATUS_AUDITING);
        // 9.2: 审核记录
        audit.setStatus(CarPackageAudit.PACKAGE_AUDIT_STATUS_PENDING);
        this.updateById(audit);
    }

    /**
     * 查看流程图
     *
     * @param instanceId 流程实例ID
     * @return 展示流程图的流
     */
    @Override
    public InputStream getProcessingImage(String instanceId) {
        // 1: 基于 流程实例ID 查询流程实例对象
        HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery()
                .processInstanceId(instanceId)
                .singleResult();

        if (processInstance == null) {
            throw new ServiceException("非法操作, 查看不了流程图!!!");
        }

        // 2: 基于 reportService接口 查询 BpmnModel 对象
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());

        // 3: 获取需要高亮的 活动ID集合
        List<HistoricActivityInstance> activityInstanceList = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(instanceId)
                .list();

        // 3.5: 把 活动实例集合 转为 活动ID集合
        List<String> highLightedActivities = activityInstanceList.stream().map(HistoricActivityInstance::getActivityId).collect(Collectors.toList());

        // 高亮流程已发生流转的线id集合
        List<String> highLightedFlowIds = getHighLightedFlows(bpmnModel, activityInstanceList);

        // 4: 返回 流程图流
        /*return processDiagramGenerator.generateDiagram(bpmnModel, highLightedActivities, Collections.emptyList(), "宋体", "宋体", "宋体");*/
        return processDiagramGenerator.generateDiagram(bpmnModel, highLightedActivities, highLightedFlowIds, "宋体", "宋体", "宋体");
    }

    /**
     * 获取已经流转的线
     *
     * @param bpmnModel                 流程文件模型对象(XML 模型对象)
     * @param historicActivityInstances 历史活动实例集合
     * @return 已经经过线的ID
     */
    /**
     *  并行网关和兼容网关不可以!!!等待去测试
     */
    private List<String> getHighLightedFlows(BpmnModel bpmnModel, List<HistoricActivityInstance> historicActivityInstances) {
        // 高亮流程已发生流转的线id集合
        List<String> highLightedFlowIds = new ArrayList<>();

        FlowNode currentFlowNode;
        // 遍历 活动实例，从每个实例的outgoingFlows中找到已执行的
        for (HistoricActivityInstance currentActivityInstance : historicActivityInstances) {

            // 获得当前活动对应的节点信息及outgoingFlows信息
            currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentActivityInstance.getActivityId(), true);
            List<SequenceFlow> sequenceFlows = currentFlowNode.getOutgoingFlows();

            for (SequenceFlow sequenceFlow : sequenceFlows) {
                for (int i = 0; i < historicActivityInstances.size() - 1; i++) {
                    HistoricActivityInstance historicActivityInstance = historicActivityInstances.get(i);

                    // 判断 历史活动ID 与 流程的目的活动ID 是否一样 ==> 表示当前 历史活动 就是下一个要去到的活动
                    if (historicActivityInstance.getActivityId().equals(sequenceFlow.getSourceRef()) && historicActivityInstances.get(i + 1).getActivityId().equals(sequenceFlow.getTargetRef())) {
                        highLightedFlowIds.add(sequenceFlow.getId());
                        break;
                    }
                }
            }
        }
        return highLightedFlowIds;
    }

    /**
     * 撤销审核
     *
     * @param id     审核记录ID
     * @param reason 撤销理由
     */
    @Override
    public void cancel(Long id, String reason) {
        // 1: 基于 审核记录ID 查询 审核记录对象
        CarPackageAudit audit = getById(id);
        if (audit == null) {
            throw new ServiceException("非法操作");
        }

        // 当前 审核记录 必须是进行中才可以被撤销
        if (!CarPackageAudit.PACKAGE_AUDIT_STATUS_PENDING.equals(audit.getStatus())) {
            throw new ServiceException("状态错误, 只有正在进行中的审核才可以被撤销!!!");
        }

        // 2: 基于 审核记录中的 服务项ID,将服务项状态修改为初始化
        serviceItemService.changeStatus(audit.getServiceItemId(), ServiceItem.AUDITSTATUS_INIT);

        // 3: 将审核记录的状态修改为撤销申请
        audit.setStatus(CarPackageAudit.PACKAGE_AUDIT_STATUS_CANCELED);
        updateById(audit);

        // 4: 调用 activiti 的 API 删除流程实例, 提供删除理由
        runtimeService.deleteProcessInstance(audit.getInstanceId(), reason);
    }

    /**
     * 更新审核记录里的 服务项
     *
     * @param serviceItem 服务对象
     * @param auditId     审核记录ID
     */
    @Override
    public void updateServiceItem(ServiceItem serviceItem, Long auditId) {
        // 根据 审核记录ID 更新审核记录对象中的服务项信息
        super.update(new LambdaUpdateWrapper<>(CarPackageAudit.class)
                .set(CarPackageAudit::getServiceItemName, serviceItem.getName())
                .set(CarPackageAudit::getServiceItemInfo, serviceItem.getInfo())
                .set(CarPackageAudit::getServiceItemPrice, serviceItem.getDiscountPrice())
                .eq(CarPackageAudit::getId, auditId));
    }
}
